﻿// Copyright (c) 2012-2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Util.Forms.Types;

namespace Util.Forms
{
    public static class ImageListExtensions
    {
        /// <summary>
        /// Gets the background color of an image list.
        /// </summary>
        public static Color GetBackgroundColor(this ImageList imageList)
        {
            int colorref = ImageList_GetBkColor(imageList.Handle);
            return Color.FromArgb(
                (colorref >> 24) & 0xFF,
                (colorref >> 0) & 0xff,
                (colorref >> 8) & 0xff,
                (colorref >> 16) & 0xff);
        }

        /// <summary>
        /// Sets the background color of an image list.
        /// </summary>
        public static void SetBackgroundColor(this ImageList imageList, Color color)
        {
            // 0x00bbggrr
            int colorref = (color.A << 24) |  (color.B << 16) | (color.G << 8) | color.R;
            ImageList_SetBkColor(imageList.Handle, colorref);
        }

        /// <summary>
        /// Draws the given image at the given index of the underlying bitmap.
        /// The image last assigned at that index must not be fully 
        /// transparent or the new image will not be visible when rendered.
        /// </summary>
        /// <param name="imageList">The image list to draw on.</param>
        /// <param name="index">Index of the image to replace.</param>
        /// <param name="newImage">The new image.</param>
        /// <remarks>
        /// When an image is assigned to the image list (by the Add method or
        /// the Item[Index] property), the image list control checks the image
        /// and remembers if the image is fully transparent (i.e. if all 
        /// pixels have alpha value 0). If the image is fully transparent, 
        /// future requests to render that image will do nothing.
        /// 
        /// The ReplaceImage() does not assign the image, but merely updates
        /// the underlying bitmap buffer. Hence the image list control has no
        /// knowledge of the change in image. If the previously assigned image
        /// is fully transparent, the updated image will not be rendered.
        /// 
        /// Therefore, the caller must make sure that the previously assigned
        /// image is not fully transparent in order for the new image to be 
        /// rendered properly. A simple way to achieve this is to create a 
        /// bitmap of 1 pixel in RGB format (i.e. without alpha channel) and
        /// assign that bitmap to the image list.
        /// </remarks>
        public static void ReplaceImage(
            this ImageList imageList, int index, Image newImage)
        {
            // We may as well assign the image at the given index to update
            // the transparency region, but this is a wasted operation if 
            // the currently assigned image is opaque. Therefore, for
            // performance reason, we comment this out.
            //imageList.Images[index] = newImage;

            // Retrieve the underlying bitmap of the image at the given index.
            using (Bitmap bmpNative = GetInPlaceBitmap(imageList, index))
            using (Graphics g = Graphics.FromImage(bmpNative))
            {
                g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
                g.DrawImage(newImage, new Rectangle(0, 0, bmpNative.Width, bmpNative.Height));
            }
        }

        public static int AddSemiTransparentImage(
            this ImageList imageList, string key, Image image)
        {
            // First, add the image to the image list.
            int index = imageList.Images.Count;
            imageList.Images.Add(key, image);

            // Next, update the pixels in the underlying bitmap if the source
            // image contains an alpha channel. In such case, the ImageList
            // control blends the image in a weird way which certainly is not
            // what we want.
            if (image.PixelFormat == PixelFormat.Format32bppArgb ||
                image.PixelFormat == PixelFormat.Format32bppPArgb)
            {
                using (Bitmap bmpNative = GetInPlaceBitmap(imageList, index))
                using (Graphics g = Graphics.FromImage(bmpNative))
                {
                    g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
                    g.DrawImage(image, new Rectangle(0, 0, bmpNative.Width, bmpNative.Height));
                }
            }

            // Return the index of the newly added image.
            return index;
        }

        /// <summary>
        /// Gets the underlying bitmap of the image at the given index of an
        /// image list. Modifications to this bitmap will change the image 
        /// list directly.
        /// </summary>
        public static Bitmap GetInPlaceBitmap(this ImageList imageList, int index)
        {
            return GetInPlaceBitmap(imageList, index,
                new Rectangle(new Point(0, 0), imageList.ImageSize));
        }

        /// <summary>
        /// Gets the given region of the underlying bitmap of the image at the
        /// given index of an image list. Modifications to this bitmap will 
        /// change the image list directly.
        /// </summary>
        public static Bitmap GetInPlaceBitmap(
            this ImageList imageList, int index, Rectangle rect)
        {
            // Retrieve the underlying large bitmap used by the image list,
            // as well as the location of the specified image.
            IMAGEINFO info;
            if (!ImageList_GetImageInfo(imageList.Handle, index, out info))
                throw new InvalidOperationException("Cannot get image info.");
            if (info.hbmImage == IntPtr.Zero)
                throw new InvalidOperationException("No bitmap is contained in the image list.");

            // Get the raw bits of the bitmap. This only works with a DIB
            // (device-independent bitmap).
            DIBSECTION dib;
            int ret = GetObject_DIBSECTION(info.hbmImage, Marshal.SizeOf(typeof(DIBSECTION)), out dib);
            if (ret != Marshal.SizeOf(typeof(DIBSECTION)))
                throw new InvalidOperationException("Cannot get bitmap data as a DIB.");
            if (dib.dsBm.bmBits == IntPtr.Zero)
                throw new InvalidOperationException("Cannot retrieve bitmap bits.");
            if (Math.Abs(dib.dsBmih.biHeight) != dib.dsBm.bmHeight)
                throw new InvalidOperationException("Inconsistent BITMAP and BITMAPINFOHEADER.");

            // Find the pixel format of the underlying bitmap.
            PixelFormat pixelFormat = PixelFormat.Undefined;
            switch (dib.dsBm.bmBitsPixel)
            {
                case 32: // pre-multiplied ARGB
                    pixelFormat = PixelFormat.Format32bppPArgb;
                    break;
                default:
                    throw new NotSupportedException("Unsupported pixel depth.");
            }

            // Convert the bounding box of the image to a .NET Rectangle.
            Rectangle bounds = new Rectangle(
                info.rcImage.left,
                info.rcImage.top,
                info.rcImage.right - info.rcImage.left,
                info.rcImage.bottom - info.rcImage.top);

            // Check that the requested rectangular region is within the
            // image's bounding box.
            if (!(rect.Left >= 0 && rect.Right <= bounds.Width &&
                rect.Top >= 0 && rect.Bottom <= bounds.Height))
            {
                throw new ArgumentException(
                    "The supplied rectangle exceeds the image's bounds.");
            }

            // Update the bounds to the requested rectangle.
            bounds = new Rectangle(
                bounds.Left + rect.Left,
                bounds.Top + rect.Top,
                rect.Width,
                rect.Height);

            // The biHeight field indicates the storage direction of the 
            // scanlines: if biHeight is positive, the scanlines are stored
            // bottom-up (the default); if biHeight is negative, the scanlines
            // are stored top-down.

            // Create a Bitmap directly on the internal buffer.
            int stride = dib.dsBm.bmWidthBytes;
            int offset;
            if (dib.dsBmih.biHeight <= 0) // top-down
            {
                offset = bounds.Top * stride +
                         bounds.Left * (dib.dsBm.bmBitsPixel / 8);
            }
            else // bottom-up
            {
                offset = (dib.dsBm.bmHeight - 1 - bounds.Top) * stride +
                         bounds.Left * (dib.dsBm.bmBitsPixel / 8);
            }

            return new Bitmap(
                bounds.Width,
                bounds.Height,
                (dib.dsBmih.biHeight > 0) ? -stride : stride,
                pixelFormat,
                dib.dsBm.bmBits + offset);
        }

        [DllImport("gdi32.dll", EntryPoint = "GetObject")]
        static extern int GetObject_DIBSECTION(IntPtr hgdiobject, int cbBuffer, out DIBSECTION pvObject);

        [DllImport("comctl32.dll")]
        static extern int ImageList_GetBkColor(IntPtr hImageList);

        [DllImport("comctl32.dll")]
        [return: MarshalAs(UnmanagedType.I4)]
        static extern void ImageList_SetBkColor(IntPtr hImageList, int clrBk);

        [DllImport("comctl32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool ImageList_GetImageInfo(IntPtr hImageList, int i, out IMAGEINFO pImageInfo);

#if false
        //[DllImport("comctl32.dll", SetLastError = true)]
        //static extern int ImageList_Add(IntPtr hImageList, IntPtr hBitmap, IntPtr hMask);

        //[DllImport("gdi32.dll", EntryPoint = "GetDIBits")]
        //static extern int GetDIBits32(IntPtr hdc, IntPtr hbmp, int uStartScan, int cScanLines, int[] bits,
        //      ref BITMAPINFOHEADER bi, int uUsage);

        [DllImport("gdi32.dll", EntryPoint = "GetDIBits")]
        static extern bool GetDIBitsInfo(IntPtr hdc, IntPtr hbmp, int uStartScan, int cScanLines, IntPtr lpvBits,
              out BITMAPINFOHEADER bi, int uUsage);

        [DllImport("gdi32.dll")]
        static extern IntPtr CreateCompatibleDC(IntPtr hdc);

        [DllImport("gdi32.dll")]
        static extern bool DeleteDC(IntPtr hdc);

        //[DllImport("gdi32.dll", SetLastError = true)]
        //static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

        //[DllImport("gdi32.dll", EntryPoint = "GetDIBits")]
        //static extern int SetDIBits32(IntPtr hdc, IntPtr hbmp, int uStartScan, int cScanLines,
        //    int[] bits, ref BITMAPINFOHEADER bi, uint fuColorUse);

        //[DllImport("gdi32.dll", EntryPoint = "GetObject")]
        //static extern int GetObjectBitmap(IntPtr hgdiobject, int cbBuffer, out BITMAP pvObject);
#endif
    }
}

namespace Util.Forms.Types
{
    [StructLayout(LayoutKind.Sequential)]
    internal struct IMAGEINFO
    {
        public IntPtr hbmImage;
        public IntPtr hbmMask;
        public int Unused1;
        public int Unused2;
        public RECT rcImage;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct RECT
    {
        public int left;
        public int top;
        public int right;
        public int bottom;
    }

    /// <summary>
    /// Note that the structure size depend on x86 or x64.
    /// </summary>
    [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly")]
    [StructLayout(LayoutKind.Sequential)]
    internal struct BITMAP
    {
        public int bmType;
        public int bmWidth;
        public int bmHeight;
        public int bmWidthBytes;
        public ushort bmPlanes;
        public ushort bmBitsPixel;
        public IntPtr bmBits;
    }

    [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly")]
    [StructLayout(LayoutKind.Sequential)]
    internal struct BITMAPINFOHEADER
    {
        public uint biSize;
        public int biWidth;
        public int biHeight;
        public ushort biPlanes;
        public ushort biBitCount;
        public BitmapCompressionType biCompression;
        public uint biSizeImage;
        public int biXPelsPerMeter;
        public int biYPelsPerMeter;
        public uint biClrUsed;
        public uint biClrImportant;
    }

    [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly")]
    [StructLayout(LayoutKind.Sequential)]
    internal struct DIBSECTION
    {
        public BITMAP dsBm;
        public BITMAPINFOHEADER dsBmih;
        public uint dsBitfields0;
        public uint dsBitfields1;
        public uint dsBitfields2;
        public IntPtr dshSection;
        public uint dsOffset;
    }

    internal enum BitmapCompressionType : uint
    {
        BI_RGB = 0,
        BI_RLE8 = 1,
        BI_RLE4 = 2,
        BI_BITFIELDS = 3,
        BI_JPEG = 4,
        BI_PNG = 5,
    }
}
